// VCX Retro Compressor
// Designed by Stige T.
// Programmed by Stige T. and Connor O.

filename:0,vu.png
filename:1,chicken_knob.png
filename:2,vcbg.png

@serialize

file_var(0, input_gain_db);
file_var(0, output_gain_db);
file_var(0, input_gain);
file_var(0, output_gain);

@init

attack = 0.1;
release = 1.5;

function w_decay(x)
( 1 - exp(-1 / (srate * x)) );
function w_freq(x)
( 1 - exp(-2 * $pi * x / srate) );

ga = 1 - w_decay(attack);
gr = 1 - w_decay(release);

t20 = w_decay(0.02);
t200 = w_decay(0.2);
t227 = w_decay(0.226746);

f025 = w_freq(0.25);
f1 = w_freq(1);
f2 = w_freq(2);
f10 = w_freq(10);
f50 = w_freq(50);
f60 = w_freq(60);
f80 = w_freq(80);
f100 = w_freq(100);
f10000 = w_freq(10000);

function limit(s low up)
( min(max(s, low), up) );
function deg(r)
( r / $pi * 180 );
function rad(d)
( d * $pi / 180 );

function follower(s atk rel)
  local(weight)
  instance(e)
(
    weight = ((e < s) ? atk : rel);
    e += (s - e) * weight;
);

function lowpass(s weight)
  instance(s1)
( s1 += (s - s1) * weight );

function highpass(s weight)
( s - this.lowpass(s, weight) );

function compressor(s)
  local(atk_mod rel_mod sa rel_env ws wse)
  instance(e fb env_atk env_rel e0 e1 e2
    flt_lo flt_hi flt_atk flt_env flt_ws flt_ws2)
(
    // feedback-driven
    // less senstitive to bass and more sensitive to highs
    fb = flt_lo.highpass(fb, f50);
    fb += flt_hi.highpass(fb, f10000) * 2;

    env_atk.follower(flt_atk.lowpass(s, f100) * input_gain, 1, t227);
    atk_mod = ga - ga / (env_atk.e + 1);

    sa = abs(fb * input_gain);
    env_rel.follower(sa, 1, t200);
    rel_mod = gr / (0.00005 * env_rel.e + 1);
    e = e0.follower(sa, 1 - atk_mod, 1 - rel_mod);
    e = e1.follower(e, 1, t20);
    e = e2.follower(e, 1, t20);
    e = flt_env.lowpass(e, f10);

    // quirk: adds constant gain to envelope (1.9dB?)
    // but waveshaper bypasses this
    wse = e;
    e = 1 / (e + 0.8);

    // quirks: weird AGC, output gain drives waveshaper
    fb = s * e;
    s = fb * output_gain * (input_gain / 50 + 1);

    // distortion causes a boost in lows
    ws = flt_ws.highpass(s, f80);

    // 18dB of drive
    ws = sin(ws * wse * 8 / $pi) / 8;

    // dampen highs, mix with signal
    s += flt_ws2.lowpass(ws, f60);
);

// graphical functions:

function xy(x y)
( gfx_x = x; gfx_y = y; );
function rgb(r g b)
( gfx_r = r; gfx_g = g; gfx_b = b; );

function rect(x_ y_ w h c i)
  instance(x y width height count index)
(
    x = x_;
    y = y_;
    width = w;
    height = h;
    count = c;
    index = i;
);

function pot_val(v)
  instance(frame val min_val max_val count height)
(
    val = v;
    frame = (val - min_val) * (count - 1) / (max_val - min_val) + 0.5;
    frame |= 0;
);

function pot_cfg(default_ min_val_ max_val_ step_)
  instance(default min_val max_val step)
(
    default = default_;
    min_val = min_val_;
    max_val = max_val_;
    step    = step_;
    this.pot_val(default);
);

function pot_draw()
  instance(x y width height index frame val)
(
    // coords implicitly 0, as is its missing fields (10 in length)
    coords[1] = frame * height;
    coords[2] = coords[6] = width;
    coords[3] = coords[7] = height;
    coords[4] = x;
    coords[5] = y;
    gfx_blitext(index, coords, 0);

    gfx_r = gfx_g = gfx_b = 1;
    gfx_x = x + width / 2 - 12;
    gfx_y = y + height + 2;
    gfx_drawnumber(val, 1);
);

function collision()
  instance(x y width height)
( mouse_x > x && mouse_x < x + width && mouse_y > y && mouse_y < y + height );

function drag_start()
  instance(dragging y_old default)
(
    !ctrl ? (
        y_old = mouse_y;
        dragging = 1;
    ) : this.pot_val(default);
);

function drag_stop()
  instance(dragging val val_old)
(
    dragging = 0;
    val_old = val;
);

function pot_drag()
  local(val)
  instance(val_old min_val max_val y_old step)
(
    val = val_old + (y_old - mouse_y) * step;
    val = limit(val, min_val, max_val);
    this.pot_val(val);
);

function draw_vum20(s)
  local(gain driver angle hand_x hand_y)
(
    gain = log10(s) * 20;
    gain = limit(gain, min_dB, max_dB);

    // inverse parabolic from p0 to p1, linear from p1 to p2
    gain < p1dB ? (
        driver = -sqrt( (gain - p1dB) / (p0dB - p1dB) ) + 1;
        driver *= p1a - p0a;
        angle = p0a + driver;
    ) : (
        driver = (gain - p1dB) / (p2dB - p1dB);
        driver *= p2a - p1a;
        angle = p1a + driver;
    );
    hand_x = hand_len * cos(angle) + gfx_x;
    hand_y = hand_len * sin(angle) + gfx_y;
    gfx_lineto(hand_x, hand_y, 1);
);

pot0.rect(41, 62, 80, 80, 61, 1);
pot1.rect(374, 62, 80, 80, 61, 1);
pot0.pot_cfg(0, -6, 50, 0.2);
pot1.pot_cfg(0, -12, 12, 0.1);

pot0.pot_val(input_gain_db);
pot1.pot_val(output_gain_db);

vu.rect(158, 58, 184, 88, 1, 0);
hand_len = 99;

min_dB = -25;
max_dB = 4.5;

p0a = rad(270 - 36.8);
p1a = rad(270 - 15.5);
p2a = rad(270 + 33.0);
p0dB = -20;
p1dB = -7;
p2dB = 3;

@gfx 500 118

mouse_hold = mouse_cap & 1;
ctrl = mouse_cap & 4;
mouse_click = mouse_hold - last_cap;
last_cap = mouse_hold;

mouse_click == 1 ? (
    pot0.collision() ? pot0.drag_start();
    pot1.collision() ? pot1.drag_start();
    vu.collision() ? vu_mode = !vu_mode;
);

mouse_click == -1 ? (
    pot0.drag_stop();
    pot1.drag_stop();
);

pot0.dragging ? pot0.pot_drag();
pot1.dragging ? pot1.pot_drag();

input_gain_db = pot0.val;
output_gain_db = pot1.val;
input_gain = 10 ^ (input_gain_db / 20);
output_gain = 10 ^ (output_gain_db / 20);

xy(vu.x, vu.y);
gfx_blit(0, 1, 0);

function v(s)
(
    xy(vu.x + 92, vu.y + 107);
    draw_vum20(s);
);

vu_mode ? (
    rgb(0.251, 0.290, 0.227);
    v(sqrt(frmsT.s1));

    rgb(0.039, 0.439, 0.969);
    v(sqrt(frmsR.s1));

    rgb(0.922, 0.176, 0.490);
    v(sqrt(frmsL.s1));
) : (
    rgb(0.890, 0.078, 0.094);
    v(sqrt(frmsM.s1));
);

xy(0, 0);
gfx_blit(2, 1, 0);

pot0.pot_draw();
pot1.pot_draw();

@sample

spl0 = comp0.compressor(spl0);
spl1 = comp1.compressor(spl1);

vu_mode ? (
    e_sqr0 = comp0.e * comp0.e;
    e_sqr1 = comp1.e * comp1.e;
    e_min = min(e_sqr0, e_sqr1);
    frmsL.lowpass(e_sqr0, f2);
    frmsR.lowpass(e_sqr1, f2);
    frmsT.lowpass(e_min, f025);
) : (
    e_mono = (comp0.e + comp1.e) / 2;
    frmsM.lowpass(e_mono * e_mono, f1);
);
